Skip to content

feat: structured logging for provider diagnostics + Cursor SDK rules/skills logs - #85

Merged
justin-carper merged 1 commit into
mainfrom
cursor-sdk-logging
Jul 30, 2026
Merged

feat: structured logging for provider diagnostics + Cursor SDK rules/skills logs#85
justin-carper merged 1 commit into
mainfrom
cursor-sdk-logging

Conversation

@rfhold

@rfhold rfhold commented Jul 29, 2026

Copy link
Copy Markdown
Collaborator

Summary

The plugin's own diagnostics, and diagnostics from the bundled @cursor/sdk, were being written straight to console.warn/console.error/console.log instead of using opencode's plugin logging API. This surfaced as raw, unstructured lines in the terminal, e.g.:

AgentSkillsCursorRulesService load completed meta={durationMs: 86, ruleCount: 18, skillCount: 18}
LocalCursorRulesService load completed meta={durationMs: 89, ruleCount: 1}

These two (plus a third, CursorPluginsAgentSkillsService load completed) come from @cursor/sdk's bundled local-exec runtime, which writes them straight to console.log — there's no public logger hook in the SDK to redirect them.

What changed

  • Plugin's own logging: every console.warn/console.error call in agent-backend.ts, agent-events.ts, and language-model.ts now routes through client.app.log({ body: { service: "opencode-cursor", level, message, extra } }), with a console.* fallback when no client bridge is available (e.g. the provider used standalone/in tests).
  • New src/provider/log-bridge.ts: mirrors the existing subagent-bridge.ts globalThis pattern so the provider layer (which has no direct import path to the plugin's opencode client) can log through it. Published by plugin/index.ts on init, cleared on dispose().
  • New src/provider/cursor-log-intercept.ts: a narrowly-scoped console.log interceptor used for the in-process transport. It matches only the three known Cursor rules/skills "load completed" messages (ANSI-safe) and re-emits them via pluginLog; every other console.log call passes through untouched.
  • Sidecar transport: src/sidecar/agent-host.mjs (the Node child that hosts @cursor/sdk when Bun's http2 client can't be used) installs the same interception in the child process and forwards matches to the parent over the existing JSONL protocol as a new { ev: "log" } message. SidecarClient gained an onLog option to receive these.

Testing

  • npm run typecheck — clean
  • npm test — 371 passed (29 pre-existing files + 2 new: log-bridge.test.ts, cursor-log-intercept.test.ts; sidecar.test.ts gained a log-forwarding case backed by a new emitRulesLog flag in the fake SDK fixture)
  • npm run build — verified the interceptor + log bridge are present in both dist/chunk-*.js (shared by provider/plugin) and dist/sidecar/agent-host.js

Notes

A broader interception of every Cursor SDK console.log call would risk swallowing unrelated output; this only recognizes the three confirmed rules/skills messages. If Cursor exposes a proper logger callback upstream, we can drop the interceptor and pass it through directly.

…skills logs

Converts every console.warn/console.error in the provider (agent-backend,
agent-events, language-model) to route through opencode's client.app.log()
API instead, with a console.* fallback when no client bridge is published.

Also captures @cursor/sdk's own internal rules/skills load-completion
diagnostics (e.g. "LocalCursorRulesService load completed
meta={durationMs, ruleCount}"), which the SDK writes straight to
console.log with no public logger hook, and re-emits them as structured
opencode logs instead of raw terminal noise:

- In-process transport: a narrowly-scoped console.log interceptor matches
  only the three known Cursor rules/skills messages; everything else
  passes through unchanged (src/provider/cursor-log-intercept.ts).
- Sidecar transport: the child process installs the same interception and
  forwards matches over the existing JSONL protocol as a new "log" event
  (src/sidecar/agent-host.mjs), which SidecarClient forwards via a new
  onLog option.

New src/provider/log-bridge.ts mirrors the existing subagent-bridge.ts
globalThis pattern to give the provider layer access to the plugin's
opencode client without a circular import.
@justin-carper
justin-carper merged commit 2299bbf into main Jul 30, 2026
6 checks passed
@justin-carper
justin-carper deleted the cursor-sdk-logging branch July 30, 2026 17:53
justin-carper added a commit that referenced this pull request Aug 3, 2026
Two conflicts, both resolved to keep each side's intent:

- language-model.ts: #85/#86 moved the resolveSystemDelivery call inside
  the withSessionLock callback. The skills-catalogue lookup was re-placed
  into that relocated call rather than restored to its old position.
- CHANGELOG.md: the skills-bridge entry stays under [Unreleased]; main's
  0.6.2 and 0.7.0 release sections are kept in full.
justin-carper added a commit that referenced this pull request Aug 3, 2026
0.7.0 (#85) moved the plugin's own diagnostics off console.* and onto
opencode's structured logging API. The skills bridge predated that and
still wrote to the terminal, leaving the only console.* calls in src/.

Withheld skills and the mirror-failure path now pass structured extra
fields instead of pre-formatted strings, so the reason codes are
queryable rather than embedded in a message.

Also corrects the plugin-cache path in the README limitation: it is
platform-dependent, as PLUGIN_CACHE_PATH shows, and was documented as
POSIX-only.
justin-carper added a commit that referenced this pull request Aug 3, 2026
…scovery (#90)

* feat: mirror opencode skills into .cursor/skills/ for Cursor agent discovery

Discover opencode's resolved skills (project + global + config.skills.paths),
filter through permission config, and materialise them as a git-ignored mirror
in <cwd>/.cursor/skills/ with a 'generated: opencode-cursor' sentinel. An
<available_skills> catalogue is appended to the generated system rule so the
Cursor agent can discover and load skills on demand.

- New: src/plugin/skill-discovery.ts — filesystem walk, frontmatter parsing,
  permission filtering (map-form + rule-array), extraPaths, path expansion,
  skillSetHash including all files (mtime + size).
- New: src/provider/skill-mirror.ts — materialisation with sentinel, git-ignore,
  idempotent writes, stale-dir pruning, user-owned protection, per-file 1MB
  skip, total 10MB cap, removeSkillMirror for dispose, buildSkillsCatalogue.
- Updated: src/provider/system-rule.ts — writeSystemRule and
  resolveSystemDelivery accept optional skillsCatalogue appended to rule body.
- Updated: src/plugin/index.ts — forwardSkills option (default true),
  skills.include/exclude override, materialisation in config hook, live
  re-sync in chat.params hook (hash-gated), currentSkillsCatalogue always
  forwarded per turn, removeSkillMirror in dispose.
- Updated: src/provider/language-model.ts — dynamic + static catalogue
  resolution (no self-provisioning — respects forwardSkills:false).
- Updated: src/provider/delegate.ts + src/plugin/cursor-tools.ts —
  settingSources: ['project'] passed to delegate's acquireAgent.
- Updated: src/provider/index.ts — skillsCatalogue in provider options.
- Tests: 403 passing (25 skill-discovery, 14 skill-mirror, 10 plugin-skill-
  mirror, 6 catalogue in language-model-system, plus existing).
- Docs: README.md (Skills section), CHANGELOG.md ([Unreleased]),
  SECURITY.md (skills mirror threat model).
- .gitignore: exclude .cursor/ (plugin-generated runtime artifacts).

* fix(skills): follow symlinked skill dirs and files

Dirent.isDirectory() is false for a symlink pointing at a directory, so
a skill linked in from a shared checkout was dropped from the mirror
with no warning. Symlinked supporting files were lost the same way.

Entries are now classified through the link target, with a resolved-path
visited set guarding the cycles that following links admits.

* docs(skills): correct deny scope, note plugin-skill gap

skills.include is checked before permissions resolve, so it re-admits a
denied skill by design — SECURITY.md read as though deny were absolute.

Skills bundled in opencode plugin packages live in the package cache,
which is not a scanned location, and the SDK exposes no skills API to
resolve them. Documented so the omission is visible rather than silent.

* style(plugin): normalize indentation in skills bridge blocks

* refactor(skills): route mirror diagnostics through pluginLog

0.7.0 (#85) moved the plugin's own diagnostics off console.* and onto
opencode's structured logging API. The skills bridge predated that and
still wrote to the terminal, leaving the only console.* calls in src/.

Withheld skills and the mirror-failure path now pass structured extra
fields instead of pre-formatted strings, so the reason codes are
queryable rather than embedded in a message.

Also corrects the plugin-cache path in the README limitation: it is
platform-dependent, as PLUGIN_CACHE_PATH shows, and was documented as
POSIX-only.

---------

Co-authored-by: Wayne Simpson <wayne@nocodecreative.io>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants